
import { Sprite, ZRenderComp } from '../../../common/component/zrComp';
import { cubicInOut, cubicIn, cubicOut, hold } from '../../../common/animation';
import { parseColorText } from '../../../common/color/color';

import { AbstractContent } from './content';
import layout from '../../compute/v1/layout';
import { AvatarSheet, AvatarDetail, loadAvatarDetails } from '../../compute/v1/avatar';
import { load, IconDef, WeaponIcon } from './weaponIcon';

const Z_MAIN = 32; // 动态

const cLayout = {
  ribbons : [
    {
      x : 0, y : 70,
      y1 : 0, y2 : 0, y3 : 0,
    },
    {
      x : 0, y : layout.content.height - 120,
      y1 : 0, y2 : 0, y3 : 0,
    },
  ],
  ribbon : {
    height : 18,
  },
  avatarSlot : {
    width : ((layout.content.width / 6) | 0) + 1,
    height : 156,
  },
  panel : {
    x : 36,
    marginTop : 30,
    y : 0, // auto
    y3 : 0, // auto
    width : layout.content.width - 72,
    title : {
      x : 0, y : 0,
      txX : 12, txY : 4,
    },
    detail : {
      x : 0, y : 30,
      width : 0, // auto
      txX: 15, txY: 5,
      txWidth : 0, // auto
      txHPerLine : 20,
      txHeight : 128,
      sliderX: 0, // auto
      sliderY: 5,
      sliderMarginLeft: 12,
      sliderMarginRight: 15,
      sliderWidth: 4,
    },
    parameter : {
      x : 0, // auto
      marginLeft : 20,
      y : 35,
      width : 0, // auto
    },
  },
  relief : {
    // relief 移动路线是, 从开始的右边界贴 content 最左边, 移动到图片中心位置在 right2
    x2 : 108,
    y2 : 374,
  },
};

const cSequence = {
  start : {
    ribbonCollapseFrom: 0,
    ribbonCollapseTo: 700,
    avatarWallShowFrom: 1000,
    avatarWallShowTo: 1700,
  },
  slotSelect : {
    ribbonMoveFrom: 0,
    ribbonMoveTo: 1000,
  },
};

const cEvent = {
  slotFocus : 'codex/slot/focus',
  slotSelected : 'codex/slot/selected',
};

(function _() {
  const { ribbons, ribbon, avatarSlot, panel } = cLayout;

  const h2 = (layout.content.height / 2) | 0;
  ribbons[0].y1 = h2 - ribbon.height;
  ribbons[1].y1 = h2;

  const slot2 = (avatarSlot.height / 2) | 0;
  ribbons[0].y2 = (h2 - ribbon.height) - slot2;
  ribbons[1].y2 = h2 + slot2;

  ribbons[0].y3 = 48;
  const offY3 = ribbons[0].y3 - ribbons[0].y2;
  ribbons[1].y3 = ribbons[1].y2 + offY3;

  panel.y = h2 + slot2 + panel.marginTop;
  panel.y3 = panel.y + offY3;
  const panelWidth = panel.width;
  const { detail: panelD } = panel;
  panelD.width = ((panelWidth - panel.parameter.marginLeft) * 0.68) | 0;
  panel.parameter.x = panel.parameter.marginLeft + panelD.width;
  panel.parameter.width = panelWidth - panel.parameter.x;

  panelD.sliderX = panelD.width - panelD.sliderMarginRight - panelD.sliderWidth;
  panelD.txWidth = panelD.sliderX - panelD.sliderMarginLeft - panelD.txX;
}());

/**
 * CodexContent
 * ├── LoadingAnim (z-index=Z_MAIN, 包含文字 path)
 * │   ├── Twilight (z-index=Z_MAIN, many)
 * ├── Ribbon (x2)
 * ├── AvatarWall
 * │   ├── AvatarSlot (x6~)
 * ├── CodexPanel
 * │   ├── CodexDetail
 * │   ├── CodexSlider
 * │   ├── CodexParameter
 * ├── Relief
 */
class CodexContent extends AbstractContent {
  constructor(parent, zrender, animation) {
    super(parent, zrender, animation);
    this.zlevels.push(Z_MAIN); // TODO

    this.baseTime = Date.now();
    this.fSwitchCleaner = this.ticker((now) => {
      if (now - this.baseTime > 20000) {
        this.deskAnimation.footerSwitch();
        this.fSwitchCleaner();
      }
    });
    global.view = this;

    this.twilights = new LoadingAnim({ parent: this });
    const rls = cLayout.ribbons;
    this.ribbon1 = new Ribbon({
      parent: this, x: rls[0].x, y: rls[0].y, direction: 0,
    });
    this.ribbon2 = new Ribbon({
      parent: this, x: rls[1].x, y: rls[1].y, direction: 1,
    });
    this.avatarWall = new AvatarWall({
      parent: this, y: rls[1].y,
    });
    this.panel = new CodexPanel({
      parent: this,
      x: cLayout.panel.x, y: cLayout.panel.y, width: cLayout.panel.width,
    });
    this.relief = new Relief({ parent: this });

    // 动画阶段相关
    this.loadingAnimFinished = false;
    this.loadAssetsFinished = false;
    this.on('loading/finish', () => this.loadingAnimFinished = true);
    this.on(cEvent.slotSelected, (_, value) => this.onSelectSlot(value));

    this.timer()
      .when(500, () => {
        loadAvatarDetails(this.getContext())
          .then(sheets => {
            this.avatarWall.setSheets(sheets);
            this.loadAssetsFinished = true;
          })
          .catch(e => '加载资源失败' + console.error(e))
      })
      .commit();
  }

  init() {
    super.init();

    // 当动画和资源都就绪
    this.tickCleaner = this.ticker(() => {
      if (this.loadAssetsFinished && this.loadingAnimFinished) {
        this.tickCleaner();
        this.onLoadReady();
      }
    });
  }

  onLoadReady() {
    this.emit('codex/start', null);

    this.ribbon1.animOnCodexStart(cLayout.ribbons[0].y1, cLayout.ribbons[0].y2);
    this.ribbon2.animOnCodexStart(cLayout.ribbons[1].y1, cLayout.ribbons[1].y2);

    this.avatarWall.animOnCodexStart(
      [cLayout.ribbons[0].y1 + cLayout.ribbon.height, 0],
      [cLayout.ribbons[0].y2 + cLayout.ribbon.height, cLayout.avatarSlot.height],
    );
  }

  onSelectSlot(value) {
    this.ribbon1.animSelectSlot(cLayout.ribbons[0].y3);
    this.ribbon2.animSelectSlot(cLayout.ribbons[1].y3);
    this.avatarWall.animSelectSlot(cLayout.ribbons[0].y3 + cLayout.ribbon.height);
    this.panel.animSelectSlot(cLayout.panel.y3, value.detail);
    this.relief.animSelectSlot(value.detail);
  }
}

/**
 * z = 18 ~ 22
 */
class LoadingAnim extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    this.useParentZRenderInstance(0, 0);
  }

  init() {
    const txDetail = layout.buildTextPath("LOADING", {spacing : 4});
    const path = txDetail.path;
    const maxH = txDetail.height * 2;
    let totalW = txDetail.width * 2;
    let right = layout.content.width - 80;
    let left = right - totalW;
    let top = layout.content.height - 70;

    // 白色的
    const WHITE_W = 40;
    const TW_DURATION = 4000;
    const WHITE_C = 0.2

    this.spWhite19 = this.createRect(WHITE_W, maxH, -WHITE_W, 0)
      .fillLinearGradient(1, 0, 0, 0, [
        {offset: 0      , color:'#FFF0'},
        {offset: WHITE_C, color:'#FFFF'},
        {offset: 1      , color:'#FFF0'},
      ])
      .z(Z_MAIN, 19)
      .position(left, top)
      .build();
    this.spTxClip19 = this.createPathFromString(path)
      .defineAsClip()
      .scale(2)
      .build();
    this.spWhite19.setClipPath(this.spTxClip19);
    this.createAnimation('spWhite19.x')
      .whenUpdated(p => this.spWhite19.moveTo(p.value, top))
      .update()
      .when(2000, left).easing(hold)
      .when(2000 + TW_DURATION + TW_DURATION * WHITE_W / totalW, right + WHITE_W)
      .commit();
    this.createAnimation('spTxClip19.x')
      .whenUpdated(p => this.spTxClip19.moveTo(p.value, 0))
      .update()
      .when(2000, 0).easing(hold)
      .when(2000 + TW_DURATION + TW_DURATION * WHITE_W / totalW, -totalW - WHITE_W)
      .commit();

    // 黄色的
    this.spTx18 = this.createPathFromString(path)
      .fill(layout.colors.yellowNormal)
      .shadow(layout.colors.yellowNormal, 8)
      .scale(2)
      .z(Z_MAIN, 18)
      .position(left, top)
      .build();
    this.spTxClip18 = this.createRect(totalW + 100, maxH + 20, -WHITE_W * WHITE_C - totalW - 100, -10)
      .defineAsClip()
      .build();
    this.spTx18.setClipPath(this.spTxClip18);
    this.createAnimation('spTxClip18.x')
      .whenUpdated(p => this.spTxClip18.moveTo(p.value / 2, 0))
      .update()
      .when(2000, 0, hold)
      .when(2000 + TW_DURATION + TW_DURATION * WHITE_W / totalW, totalW + WHITE_W)
        .callback(() => this.emit('loading/finish', null))
      .commit();

    /** @type {Twilight[]} 动画时间 2000 ~ 2000+TW_DURATION ms */
    this.twilights = [];
    const now = Date.now();
    for (let xx = left; xx <= right; xx++) {
      let tw = new Twilight({
        parent: this,
        x: xx, y: (Math.random() * maxH + top) | 0,
        time: ((((xx - left) / (right - left)) * TW_DURATION) | 0) + 2000 + now,
      });
      this.twilights.push(tw);
    }

    this.onCodexStartCleaner = this.on('codex/start', () => this.prepareForDestroy());
  }

  prepareForDestroy() {
    this.onCodexStartCleaner();
    this.spWhite19 && this.spWhite19.destroy() && (this.spWhite19 = null);
    this.createAnimation('tx.opacity')
      .whenUpdated(p => this.spTx18.opacity(p.value))
      .update()
      .when(500, 1)
      .when(1000, 0).callback(() => this.destroy())
      .commit();
  }
}

const L1 = 2;
const L2 = 20;
const L3 = 200;

const TWILIGHT_DURATION = 2000;
const TWILIGHT_LINE = 1000;
const TWILIGHT_SPEED = TWILIGHT_LINE / TWILIGHT_DURATION;

const LINE_DEFS = [
  { stroke: 4, width: L1, opacity: 1 },
  { stroke: 3, width: L2, opacity: 0.6 },
  { stroke: 2, width: L3, opacity: 0.3 },
];

/**
 * 整个流星的动画是 1.5 秒
 * z = 22
 */
class Twilight extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    this.useParentZRenderInstance(args.x, args.y);

    /** @type {number} 到达目标位置的时间, 离现在多少时间差 (ms). 目标位置是 [0, 0] */
    this.targetTime = args.time;

    const colors = layout.colors;
    this.color = [colors.yellowNormal, colors.strokeNormal, colors.nameTagNormal][(Math.random() * 3) | 0];
  }

  init() {
    // 分开来 build 是为了防止同一时间的计算量过多
    this.buildCleaner = this.ticker((now) => {
      if (this.targetTime - now > TWILIGHT_DURATION) {
        return;
      }

      this.buildCleaner();
      // 画线
      let path = this.buildLines();
      this.spLines = LINE_DEFS.map(def => this.createPathFromString(path)
        .fillNone()
        .stroke(this.color, def.stroke)
        .opacity(def.opacity)
        .lineDash([def.width, TWILIGHT_LINE], -TWILIGHT_LINE)
        .z(Z_MAIN, 22)
        .build());
      this.lineCleaner = this.ticker((now) => this.tickLine(now));
    });
  }

  /**
   * @param {number} now 
   */
  tickLine(now) {
    let offset = (this.targetTime - now) * -TWILIGHT_SPEED;
    if (offset > 0 && this.spRect == null) {
      this.spRect = this.createRect(6, 6, -3, -3)
          .fill('#FFF')
          .opacity(0)
          .shadow(this.color, 6, 0, 0)
          .z(Z_MAIN, 22)
          .build();
      this.createAnimation('spRect', 0)
          .whenUpdated(p => this.spRect.opacity(p.value))
          .update()
          .when(0, 0)
          .when(300, 1)
          .when(1000, 0).callback(() => this.destroy())
          .commit();
    }

    this.spLines.forEach((sp, index, array) => {
      if (!sp) return;
      const def = LINE_DEFS[index];
      if (offset - def.width > 0) {
        sp.destroy();
        array[index] = null;
        return;
      }
      sp.style().lineDash([def.width, TWILIGHT_LINE], offset).update();
    });
  }

  /**
   * 线长: (TWILIGHT_LINE), 流星动画行进速度: 500px/s = 0.5px/ms (TWILIGHT_SPEED)
   */
  buildLines() {
    let remain = TWILIGHT_LINE + L3;
    let paths = ['M0,0'];

    while (remain > 0) {
      let d = (Math.random() * 100 + 30) | 0;

      switch ((Math.random() * 3) | 0) {
        case 0: {// 左:
          paths.push('h-' + d);
        } break;
        case 1: {// 左上:
          paths.push(`l-${d}-${d}`);
          d = (Math.hypot(d, d) | 0);
        } break;
        case 2: {// 上:
          paths.push(`v-${d}`);
        } break;
      }
      remain -= d;
    }

    return paths.join('');
  }
}

/**
 * z = 40 ~ 41
 */
class Ribbon extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    this.useParentZRenderInstance(args.x, args.y);

    /** @type {number} 文字移动方向: 0 向左, 1 向右 */
    this.direction = args.direction;
  }

  init() {
    const {content, colors} = layout;

    // bg
    this.bg = this.createRect(content.width, cLayout.ribbon.height)
      .fill(this.bgColor = colors.strokeBgL1)
      .opacity(0.6)
      .z(Z_MAIN, 40)
      .build();
    this.ticker(now => this.updateBgColor(now));

    // text: INCOMING TARGETS
    const txDetail = layout.buildTextPath("INCOMING TARGETS");
    const txWidth = (txDetail.width * 1.2) | 0;
    const txCount = ((layout.content.width / txWidth) | 0) + 2;

    const yy = ((cLayout.ribbon.height - txDetail.height) / 2);
    this.spTxs = new Array(txCount).fill().map((_, index) => {
      return this.createPathFromString(txDetail.path)
        .fill(layout.colors.strokeNormal)
        .position((index - 1) * txWidth, yy)
        .z(Z_MAIN, 41)
        .build();
    });
    this.ticker(now => {
      let offX = (now % 1200) / 1200 * txWidth;
      if (this.direction == 0) {
        offX = txWidth - offX;
      }
      this.spTxs.forEach((sp, index) => {
        sp.moveTo((index - 1) * txWidth + offX, yy);
      })
    });
  }

  /**
   * @param {number} now 
   */
  updateBgColor(now) {
    const {colors} = layout;
    const bgColors = [colors.strokeBgL3, colors.strokeBgL2, colors.strokeBgL1];
    const color = bgColors[((now % 300) / 100) | 0];
    if (this.bgColor == color) {
      return;
    }
    this.bg.style().fill(this.bgColor = color).update();
  }

  /**
   * @param {number} y1
   * @param {number} y2
   */
  animOnCodexStart(y1, y2) {
    this.getAnimY()
      .update()
      .when(cSequence.start.ribbonCollapseFrom, this.y)
      .when(cSequence.start.ribbonCollapseTo, y1).easing(cubicInOut)
      .when(cSequence.start.avatarWallShowFrom, y1).easing(hold)
      .when(cSequence.start.avatarWallShowTo, y2).easing(cubicInOut)
      .commit();
  }

  /**
   * @param {number} y
   */
  animSelectSlot(y) {
    this.getAnimY().update()
      .when(cSequence.slotSelect.ribbonMoveFrom, this.y)
      .when(cSequence.slotSelect.ribbonMoveTo, y)
      .commit();
  }
}

class AvatarWall extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    this.useParentZRenderInstance(0, args.y);

    this.height = 0;
    this.slots = new Array(6).fill().map((_, index) => new AvatarSlot({
      parent: this, x: cLayout.avatarSlot.width * index,
    }));
  }

  /**
   * @param {number[]} from [y, height]
   * @param {number[]} to [y, height]
   */
  animOnCodexStart(from, to) {
    const {avatarWallShowFrom:T1, avatarWallShowTo:T2} = cSequence.start;
    this.getAnimY()
      .update()
      .when(T1, from[0]).easing(hold)
      .when(T2, to[0]).easing(cubicInOut)
      .commit();
    this.createAnimation('height')
      .whenUpdated(p => this.slots.forEach(s => s.setHeight(p.value)))
      .update()
      .when(T1, from[1]).easing(hold).callback(() => {
        const countCenter = this.slots.length / 2 - 0.5;
        const offTime = this.slots.map((_, index) =>
          200 + Math.abs(index - countCenter) * 120 + ((index > countCenter) ? 60 : 0));
        this.slots.forEach((s, index) => s.initSprite(offTime[index]));
      })
      .when(T2, to[1]).easing(cubicInOut)
      .commit();

    this.tickCleaner = this.ticker(() => {
      if (this.slots.every(s => s.initAnimFinished)) {
        this.tickCleaner();
        this.animFocusSlot();
      }
    });
  }

  /**
   * 动画的第二部分, 选择 slot 显示
   */
  animFocusSlot() {
    const slotCount = this.slots.length;
    const selIdx = (Math.random() * slotCount) | 0;
    const slotCount2 = (slotCount / 2) | 0;

    const delay1 = 500;
    const offtime1 = 400;

    let focusSlot = (/** @type {number} */ index) => {
      this.slots[index].animFocus();
      this.emit(cEvent.slotFocus, { index, detail: this.slots[index].detail });
    };

    const timer = this.timer();
    let lastFocusTime;
    if (selIdx >= slotCount2) { // 动画从左至右
      for (let i = 0; i < selIdx; i++) {
        timer.when(delay1 + offtime1 * i, () => focusSlot(i))
             .when(delay1 + offtime1 * i + offtime1, () => this.slots[i].animRemoveFocus());
      }
      lastFocusTime = delay1 + offtime1 * selIdx;
    } else { // 动画从右至左
      for (let i = slotCount - 1; i > selIdx; i--) {
        timer.when(delay1 + offtime1 * (slotCount - 1 - i), () => focusSlot(i))
        .when(delay1 + offtime1 * (slotCount - i), () => this.slots[i].animRemoveFocus());
      }
      lastFocusTime = delay1 + offtime1 * (slotCount - 1 - selIdx);
    }

    timer.when(lastFocusTime, () => focusSlot(selIdx));
    timer.when(lastFocusTime + 1000, () => {
      this.slots[selIdx].animFocus();
      this.emit(cEvent.slotSelected, { index: selIdx, detail: this.slots[selIdx].detail });
    })
    .commit();
  }

  /**
   * 动画的第三部分, 确定选择 slot 显示
   * 
   * @param {number} y
   */
  animSelectSlot(y) {
    this.getAnimY().update()
      .when(cSequence.slotSelect.ribbonMoveFrom, this.y)
      .when(cSequence.slotSelect.ribbonMoveTo, y)
      .commit();
  }

  /**
   * @param {AvatarSheet[]} sheets
   */
  setSheets(sheets) {
    /** @type {AvatarDetail[]} */
    this.details = sheets.reduce((p, v) => {
      p.push(...v.instances);
      return p;
    }, []);

    const {width: w, height: h} = cLayout.avatarSlot;
    this.details.forEach(d => {
      if (d.avatar) return;

      const p = d.painter.createPainter(d.offx, d.offy, w, (h * 1.3) | 0);
      // 未选中时
      p.palette = p.palette.map(c => {
        if (c.a == 0) return c;
        if (c.b > 232) return parseColorText(layout.colors.strokeBgL2);
        return parseColorText(layout.colors.strokeBg);
      });
      d.avatar = p.to2dCanvas();

      // 选中时
      p.resetPalette();
      p.palette = p.palette.map(c => {
        if (c.a == 0) return c;
        if (c.b > 232) return parseColorText(layout.colors.strokeNormal);
        else if (c.b > 186) return parseColorText(layout.colors.strokeD1);
        else if (c.b > 107) return parseColorText(layout.colors.strokeD2);
        else return parseColorText(layout.colors.strokeBg);
      });
      d.avatar2 = p.to2dCanvas();
    });

    this.slots.forEach(s => s.setSheet(this.details[(this.details.length * Math.random()) | 0]));
  }
}

/**
 * 开始时是不显示的. 当传入 setHeight 后出现内容.
 * z = 19 ~ 32
 */
class AvatarSlot extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    this.useParentZRenderInstance(args.x, 0);
    this.width = cLayout.avatarSlot.width;
    this.height = this.avatarOffY = this.destAvatarOffY = 0;

    this.initAnimFinished = false;
  }

  /**
   * @param {number} height 
   */
  setHeight(height) {
    this.height = height;
    if (this.border == null) {
      return;
    }

    this.border.rectShape().height(height).update();
    const yy = (height - cLayout.avatarSlot.height) / 2 - this.avatarOffY;
    this.avatar1.moveTo(0, yy);
    this.avatarClip1.rectShape().height(height).update();
    this.avatar2.moveTo(0, yy);
    this.avatarClip2.rectShape().height(height).update();
  }

  /**
   * @param {number} offTime 启动展开动画的时间点
   */
  initSprite(offTime) {
    const {colors} = layout;
    this.border = this.createRect(this.width, this.height, 0, 0)
      .fill(colors.strokeBg)
      .z(Z_MAIN, 30)
      .build();

    const bgColors = [colors.strokeBgL3, colors.strokeBgL2, colors.strokeBgL1];
    this.createAnimation('border.width')
      .whenUpdated(p => {
        const color = p.value == 2 ? colors.strokeBgL3 :
          bgColors[(p.time % (60 * bgColors.length) / 60) | 0];
        this.border
          .rectShape().width(p.value).update()
          .style().fill(color).update();
      })
      .update()
      .when(offTime, this.width).easing(hold)
      .when(offTime + 500, 2).easing(cubicOut)
      .commit();

    this.avatar1.opacity(1);
    this.createAnimation('avatarOffY')
      .whenUpdated(p => {
        this.avatarOffY = p.value;
        const yy = (this.height - cLayout.avatarSlot.height) / 2 - p.value;
        this.avatar1.moveTo(0, yy);
        this.avatarClip1.moveTo(0, -yy);
        this.avatar2.moveTo(0, yy);
        this.avatarClip2.moveTo(0, -yy);
      })
      .update()
      .when(offTime, this.destAvatarOffY).easing(hold)
      .when(offTime + 500, 0).easing(cubicOut).callback(() => this.initAnimFinished = true)
      .commit();
  }

  /**
   * @param {AvatarDetail} detail 
   */
  setSheet(detail) {
    this.detail = detail;

    const avatar = detail.avatar;
    this.destAvatarOffY = avatar.height - cLayout.avatarSlot.height;
    this.avatarOffY = -this.destAvatarOffY;
    this.avatar1 = this.createImage(avatar)
      .position(0, -cLayout.avatarSlot.height / 2)
      .opacity(0)
      .z(Z_MAIN, 20)
      .build();
    this.avatarClip1 = this.createRect(this.width, 0)
      .defineAsClip()
      .build();
    this.avatar1.setClipPath(this.avatarClip1);

    this.avatar2 = this.createImage(detail.avatar2)
      .position(0, -cLayout.avatarSlot.height / 2)
      .opacity(0)
      .z(Z_MAIN, 21)
      .build();
    this.avatarClip2 = this.createRect(this.width, 0)
      .defineAsClip()
      .build();
    this.avatar2.setClipPath(this.avatarClip2);
  }

  animFocus() {
    let spFocusRect = this.createRect(this.width, 0)
      .fill(layout.colors.strokeNormal)
      .z(Z_MAIN, 31)
      .build();

    this.createAnimation('spFocusRect.height')
      .whenUpdated(p => {
        if (p.value <= 1) {
          spFocusRect.moveTo(0, (1 - p.value) * this.height);
          spFocusRect.rectShape().height(p.value * this.height).update();
        } else if (spFocusRect) {
          spFocusRect.moveTo(0, 0);
          spFocusRect.rectShape().height((2 - p.value) * this.height).update();
        }
      })
      .update()
      .when(0, 0)
      .when(100, 1).easing(cubicIn)
      .when(240, 1).easing(hold).callback(() => this.avatar2.opacity(1))
      .when(340, 2).easing(cubicOut).callback(() => { spFocusRect.destroy(); spFocusRect = null; })
      .commit();
  }

  animRemoveFocus() {
    this.avatar2.opacity(0);
  }
}

/**
 * 开始时是不显示的. 当 cEvent.slotFocus 事件触发后出现内容.
 * 
 * 标题 z = 50
 * 文本 z = 50
 * 滑动条 z = 50 ~ 60
 * 右边参数 z = 50
 */
class CodexPanel extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    this.useParentZRenderInstance(args.x, args.y);
    /** @type {number} */ this.width = args.width;

    this.on(cEvent.slotFocus, (_, value) => {
      this.onSlotFocus(value.detail);
    });

    const { detail:layDt, parameter:layPm } = cLayout.panel;
    this.detail = new CodexDetail({
      parent: this,
      x: layDt.x + layDt.txX,
      y: layDt.y + layDt.txY,
      width: layDt.txWidth, height: layDt.txHeight,
      hPerLine: layDt.txHPerLine,
    });
    this.slider = new CodexSlider({
      parent: this,
      x: layDt.x + layDt.sliderX,
      y: layDt.y + layDt.sliderY,
      width: layDt.sliderWidth,
      height: layDt.txHeight,
    });
    this.parameter = new CodexParameter({
      parent: this,
      x: layPm.x, y: layPm.y, width: layPm.width,
    });

    this.detail.onViewChanged = (all, from, to) => this.slider.onViewChanged(all, from, to);
  }

  /**
   * @param {AvatarDetail} detail 
   */
  onSlotFocus(detail) {
    // clean previous
    if (this.spTitle) {
      this.removeAndForceAnimation('spTitle.x');
      this.spTitle.destroy();
    }

    // current
    const pathObj = layout.buildTextPath(detail.name.toUpperCase(), { spacing : 2 });
    this.spTitle = this.createPathFromString(pathObj.path)
      .fill(layout.colors.yellowNormal)
      .z(Z_MAIN, 50)
      .position(layout.content.width, cLayout.panel.title.txY)
      .build();
    this.createAnimation('spTitle.x')
      .whenUpdated(p => this.spTitle.moveTo(p.value, cLayout.panel.title.txY))
      .update()
      .when(0, layout.content.width)
      .when(300, cLayout.panel.title.txX).easing(cubicOut)
      .commit();
  }

  /**
   * @param {number} y 
   * @param {AvatarDetail} detail 
   */
  animSelectSlot(y, detail) {
    this.getAnimY().update()
      .when(cSequence.slotSelect.ribbonMoveFrom, this.y)
      .when(cSequence.slotSelect.ribbonMoveTo, y)
      .commit();

    const { detail:layDt, parameter:layPm } = cLayout.panel;
    this.spDetailBg = this.createRect(layDt.width, 300) // for debug
      .position(layDt.x, layDt.y)
      .fill('#FFF1')
      .z(Z_MAIN, 0)
      .build();

    // 写文字
    this.detail.animSelectSlot(detail.details.toUpperCase());
    this.slider.animSelectSlot();
    this.parameter.animSelectSlot(detail);
  }
}

class CodexDetail extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    this.useParentZRenderInstance(args.x, args.y);

    /** @type {number} */ this.width = args.width;
    /** @type {number} */ this.height = args.height;
    /** @type {number} */ this.hPerLine = args.hPerLine;

    /**
     * 表示当前显示区域, 最下方的 y
     */
    this.targetBottomY = this.height;
    this.viewBottomY = this.height;
  }

  /**
   * @param {number} allHeight 
   * @param {number} viewFrom 
   * @param {number} viewTo 
   */
  onViewChanged = (allHeight, viewFrom, viewTo) => {}

  /**
   * @param {string} chs 
   */
  animSelectSlot(chs) {
    const { width, height, hPerLine } = this;
    if (this.spChs) {
      this.spChs.forEach(sp => sp.sp.destroy());
      this.spChClip.destroy();
    }
    /** @type {{sp:Sprite, top:number, bottom:number}[]} */
    const spChs = this.spChs = [];

    // let currentLine = 0; // 当前是第几行
    let xx = 0;
    let yy = 0;
    const spacing = 2;
    // const maxLine = layDt.txMaxLine;  先不考虑翻页
    let fNextLine = () => {
      // currentLine++;
      xx = 0;
      yy += hPerLine;
    }

    const spChClip = this.spChClip = this.createRect(width, height)
      .defineAsClip()
      .build();

    // replaceAll 保证每个换行符后面一定有空格
    const txs = chs.replaceAll('\n', '\n ').split(' ').filter(str => str.length);
    const spaceObj = layout.buildTextPath(' ');
    for (let i = 0; i < txs.length; i++) {
      // 一个单词必须放同一行 (除非整个单词一行都放不下, 暂时不考虑)
      // 空格可以放末尾, 可以超出范围
      // 标点、换行符和它前面的单词一同视为一个单词
      // 只有放不下, 或者遇到换行符, 才进行换行
      let word = txs[i];
      let objs = new Array(word.length).fill().map((_, index) => layout.charPathFactory(word[index]));
      let totalW = objs.reduce((p, v) => p + v.width, 0) + spacing * (word.length - 1);

      if (xx + totalW > width) { // 另起一行
        fNextLine();
      }

      objs.forEach(obj => {
        let spCh = this.createPathFromString(obj.fPath(xx, yy))
          .fill(layout.colors.strokeNormal)
          .opacity(0)
          .z(Z_MAIN, 50)
          .build();
        spCh.setClipPath(spChClip);
        spChs.push({ sp: spCh, top: yy, bottom: yy + obj.height });
        xx += obj.width + spacing;
      });

      if (word.endsWith('\n')) {
        fNextLine();
      } else { // 再加一个空格
        xx += spaceObj.width + spacing;
      }
    }

    let showTxFunc = (index) => (() => {
      const obj = spChs[index];

      this.createAnimation('spChs-' + index)
        .whenUpdated(p => obj.sp.opacity(p.value))
        .update()
        .when(0, 1)
        .when(200, 0.5)
        .commit();

      if (obj.bottom > this.targetBottomY) {
        this.moveViewY(obj.bottom);
      }
    });

    const timer = this.timer();
    for (let i = 0; i < spChs.length; i++) {
      timer.when(i * 40, showTxFunc(i));
    }
    timer.commit();
  }

  /**
   * @param {number} targetBottomY 
   */
  moveViewY(targetBottomY) {
    this.targetBottomY = targetBottomY;
    const { height, viewBottomY } = this;

    this.createAnimation('bottomY')
      .whenUpdated(p => {
        this.viewBottomY = p.value;
        this.spChClip?.moveTo(0, p.value - height);
        this.spChs.forEach(chs => {
          chs.sp.moveTo(0, height - p.value);
        });
        if (this.onViewChanged) {
          this.onViewChanged(targetBottomY, p.value - height, p.value);
        }
      })
      .update()
      .when(0, viewBottomY)
      .when(Math.abs(targetBottomY - viewBottomY) * 10, targetBottomY)
      .commit();
  }
}

class CodexSlider extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    this.useParentZRenderInstance(args.x, args.y);

    /** @type {number} */ this.width = args.width;
    /** @type {number} */ this.height = args.height;
    /** @type {number} */ this.signH = 8;
    /** @type {number} */ this.sliderY = (this.signH * 1.5) | 0;
    /** @type {number} */ this.sliderH = this.height - this.sliderY * 2;
    this.from = 0;
    this.to = 1;
  }

  /**
   * @param {number} allHeight 
   * @param {number} viewFrom 
   * @param {number} viewTo 
   */
  onViewChanged(allHeight, viewFrom, viewTo) {
    const from = this.from = viewFrom / allHeight;
    const to = this.to = viewTo / allHeight;
    const { sliderH, spRect, spTop, spBottom } = this;
    if (spRect == null) return;

    if (from == 0 && to == 1) {
      spRect.opacity(0);
      spTop.opacity(0);
      spBottom.opacity(0);
      return;
    }
    spRect.rectShape().height((to - from) * sliderH).y(from * sliderH).update().opacity(1);
    spTop.opacity(from == 0 ? 0 : 1);
    spBottom.opacity(to == 1 ? 0 : 1);
  }

  animSelectSlot() {
    const { width, height, from, to, sliderY, sliderH, signH } = this;
    this.spBg = this.createRect(width, sliderH)
      .fill(layout.colors.strokeBg)
      .opacity(0.7)
      .position(0, sliderY)
      .z(Z_MAIN, 50)
      .build();
    this.spRect = this.createRect(width, (to - from) * sliderH, 0, from * sliderH)
      .fill(layout.colors.strokeD1)
      .shadow(layout.colors.strokeBgL3, 6)
      .opacity(0)
      .position(0, sliderY)
      .z(Z_MAIN, 51)
      .build();

    this.spTopBg = this.createPathFromString('M-2,0h8v8h-2v-2h-2v-2h-2v-2h-2v-2Z')
      .fill(layout.colors.strokeBg)
      .opacity(0.7)
      .z(Z_MAIN, 50)
      .build();
    this.spBottomBg = this.createPathFromString('M4,0v8h-8v-2h2v-2h2v-2h2v-2h2Z')
      .fill(layout.colors.strokeBg)
      .opacity(0.7)
      .position(0, height - signH)
      .z(Z_MAIN, 50)
      .build();
      
    this.spTop = this.createPathFromString('M-2,0h8v8h-2v-2h-2v-2h-2v-2h-2v-2Z')
      .fill(layout.colors.strokeD1)
      .shadow(layout.colors.strokeBgL3, 6)
      .opacity(0)
      .z(Z_MAIN, 51)
      .build();
    this.spBottom = this.createPathFromString('M4,0v8h-8v-2h2v-2h2v-2h2v-2h2Z')
      .fill(layout.colors.strokeD1)
      .shadow(layout.colors.strokeBgL3, 6)
      .opacity(0)
      .position(0, height - signH)
      .z(Z_MAIN, 51)
      .build();

  }
}

class CodexParameter extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    this.useParentZRenderInstance(args.x, args.y);
    /** @type {number} */ this.width = args.width;
    /** @type {WeaponIcon[]} */ this.icons = [];
  }

  /**
   * @param {AvatarDetail} detail 
   */
  animSelectSlot(detail) {
    // HP ___/ 06
    // ||||||||||
    this.hpSpTitles = this.showTitle({ delay:0, y:0, title:'HP', marginRight:20 });
    this.hpSpTitles.then = () => {
      this.showDigit({ x:0, y:0, value:detail.HP | 0 });
    };

    // AT ___/ 03
    // ||||||||||
    this.atSpTitles = this.showTitle({ delay:400, y:40, title:'AT', marginRight:20 });
    this.atSpTitles.then = () => {
      this.showDigit({ x:0, y:40, value:detail.AT | 0 });
    };

    // weekness
    this.weakSpTitles = this.showTitle({ delay:800, y:80, title:'WEAKS', marginRight:0 });
    this.weakSpTitles.then = () => {
      load(this.getContext()).then(bundles => {
        const bundle = bundles.find(b => b.category === 'megaman');
        if (bundle == null) return;
        const weapons = bundle.weapons.filter(w => detail.weakness.includes(w.name));
        if (!weapons.length) return;
        this.showWeakness(weapons, 80);
      });
    };
  }

  /**
   * @param {{delay:number,y:number,title:string,marginRight:number}} args
   */
  showTitle(args) {
    const { width } = this;
    const { delay, y, title, marginRight } = args;
    const { path:pathTitle, width:pathW } = layout.buildTextPath(title);
    const spTitle = this.createPathFromString(pathTitle)
      .fill(layout.colors.redNormal)
      .position(0, y)
      .z(Z_MAIN, 50)
      .build();
    const spSplit = this.createPathFromString('M0,6h2v-2h2v-2h2v-2h2v2h-2v2h-2v2h-2v2h-2v-2Z')
      .fill(layout.colors.redNormal)
      .z(Z_MAIN, 50)
      .position(pathW + 2, y)
      .build();
    const spLine = this.createRect(0, 2, 0, 8)
      .fill(layout.colors.redNormal)
      .z(Z_MAIN, 50)
      .position(pathW + 2, y)
      .build();
    spTitle.hide();
    spSplit.hide();
    spLine.hide();
    
    let obj = {
      spTitle, spSplit, spLine,
      destroy: () => {
        spTitle.destroy();
        spSplit.destroy();
        spLine.destroy();
      },
      then: () => {},
    };

    this.timer()
      .when(delay, () => spTitle.show())
      .when(delay + 200, () => {
        spSplit.show();
        spLine.show();
        this.createAnimation('spTitle.' + title)
          .whenUpdated(p => {
            spSplit.moveTo(pathW + 2 + p.value, y);
            spLine.rectShape().width(p.value).update();
          })
          .update()
          .when(0, 0)
          .when(400, width - 6 - pathW - marginRight - spSplit.getBoundingRect().width).easing(cubicOut)
          .commit();
      })
      .when(delay + 600, () => obj.then?.call(this))
      .commit();

    return obj;
  }

  /**
   * @param {{x:number,y:number,value:number}} args
   */
  showDigit(args) {
    const { width } = this;
    const { value, x, y } = args;
    const timer = this.timer();

    const len = (width / 8) | 0;
    const spProcesses = new Array(len).fill().map((_, index) => 
      this.createRect(4, 10, width - 4 - index * 8, 16)
        .fill(layout.colors.strokeBgD1)
        .position(x, y)
        .z(Z_MAIN, 50)
        .build())

    /** @type {Sprite} */ let spDigit;
    for (let i = 0; i <= value && i < 100; i++) {
      timer.when(i * 50, () => {
        if (spDigit) spDigit.destroy();
        let text = (i > 9) ? '' + i : '0' + i;
        let obj = layout.buildTextPath(text);
        spDigit = this.createPathFromString(obj.path)
          .fill(layout.colors.redNormal)
          .position(x + width - obj.width, y)
          .z(Z_MAIN, 50)
          .build();

        let sp = spProcesses[i - 1];
        if (sp) {
          sp.style()
            .fill(layout.colors.strokeD1)
            .shadow(layout.colors.strokeNormal, 6)
            .update();
        }
      })
      .commit();
    }
  }

  /**
   * @param {IconDef[]} defs
   * @param {number} y
   */
  showWeakness(defs, y) {
    const { width } = this;
    const iconW = defs[0].width;
    const maxLen = 4;
    const marginRight = iconW >> 2;
    const scale = 1.5;
    const yy = y + 16;

    const timer = this.timer();
    defs.slice(0, maxLen).forEach((def, index, array) => {
      timer.when(index * 100 + 50, () => {
        const icon = new WeaponIcon({
          parent: this,
          x: width - (array.length - index) * (iconW + marginRight) * scale + marginRight * scale,
          y: yy,
          iconDef: def, scale,
          zIndex: Z_MAIN, z: 50,
        });
        this.icons.push(icon);
      });
    });
    timer.commit();
  }
}

/**
 * z = 5 ~ 11
 */
class Relief extends ZRenderComp {
  constructor(args) {
    super(args.parent);
    this.useParentZRenderInstance();

    /** @type {Sprite} */ this.spBg = null;
    /** @type {Sprite} */ this.spBgGlow = null;
    /** @type {Sprite} */ this.spBgClip = null;
    /** @type {Sprite} */ this.spLight = null;
  }

  /**
   * @param {AvatarDetail} detail 
   */
  animSelectSlot(detail) {
    const img = detail.painter;
    if (this.spBg) {
      this.spBg.destroy();
      this.spBgGlow.destroy();
      this.spBgClip.destroy();
      this.spLight.destroy();
      this.spBg = null;
      this.bgTickCleaner();
    }
    const x1 = this.x = -img.width;
    const x2 = cLayout.relief.x2 - img.width / 2;
    this.y = cLayout.relief.y2 - img.height / 2;

    Promise.all([
      new Promise((resolve) => {
        resolve(img.toPath(c => c.a > 0).path);
      }),
      new Promise((resolve) => {
        resolve(img.toPath(c => c.b > 232).path);
      }),
    ]).then((/** @type {string[]}*/ paths) => {
      if (this.spBg) return;
      const spBg = this.spBg = this.createPathFromString(paths[0])
        .fill(layout.colors.strokeBgD3)
        .opacity(0)
        .z(Z_MAIN, 5)
        .build();

      this.spBgGlow = this.createRect(img.width, img.height)
        .fillLinearGradient(0, 0, 1, 0.5, [
          { offset: 0.4 , color: '#FFF0' },
          { offset: 0.41, color: '#FFF3' },
          { offset: 0.5 , color: '#FFF3' },
          { offset: 0.51, color: '#FFF0' },
          { offset: 0.59, color: '#FFF0' },
          { offset: 0.6 , color: '#FFF3' },
          { offset: 0.62, color: '#FFF3' },
          { offset: 0.63, color: '#FFF0' },
        ])
        .z(Z_MAIN, 6)
        .build();
      this.spBgClip = this.createPathFromString(paths[0])
        .defineAsClip()
        .build();
      this.spBgGlow.setClipPath(this.spBgClip);

      this.getAnimX()
        .update()
        .when(400, x1).easing(hold).callback(() => spBg.opacity(0.5))
        .when(1000, x2).easing(cubicOut)
        .commit();

      const spLight = this.spLight = this.createPathFromString(paths[1])
        .fill(layout.colors.strokeBgL1)
        .opacity(0)
        .z(Z_MAIN, 7)
        .build();

      this.bgTickCleaner = this.ticker(now => {
        let offX = ((now % 6000) / 2000 - 1) * img.width;
        this.spBgGlow.moveTo(offX);
        this.spBgClip.moveTo(-offX);

        let alphaLight = Math.abs((now % 5000) / 2500 - 1);
        spLight.opacity(cubicInOut(alphaLight));
      });
    });
  }
}

export {
  CodexContent
};
